"""Adapter that uses kconfiglib to parse Kconfig files and produce the
same lightweight model used by the GUI (parser/config_model.py).

This file provides helper methods and a single parse_file orchestration
entry that preserves previous behavior while being easier to maintain.
"""

from typing import List, Any, Dict, Optional
import kconfiglib
from .config_model import (
    MenuItem,
    ConfigItem,
    CommentItem,
    ChoiceItem,
    ConfigProperty,
    DefaultValue,
    Condition,
    Selection,
    RangeRestriction,
)
import os
import glob
import logging
from .expression_evaluator import ExpressionEvaluator

logger = logging.getLogger(__name__)


class KconfiglibAdapter:
    def __init__(self):
        # store parsed symbols in a dict
        self.config_items: Dict[str, ConfigItem] = {}
        self.menu_items: Dict[str, MenuItem] = {}
        self.comment_items: Dict[str, CommentItem] = {}
        self.choice_items: Dict[str, ChoiceItem] = {}

    def _prepare_env(self, file_path: str, var_map: Optional[Dict[str, str]]):
        """Prepare environment for parsing. Returns (old_env, vars_in_file).

        old_env: mapping of environment vars we replaced so the caller can restore.
        vars_in_file: set of $VARNAME occurrences found in the file (heuristics)
        """
        old_env: Dict[str, Optional[str]] = {}
        vars_in_file = set()
        try:
            with open(file_path, 'r', encoding='utf-8') as fh:
                import re

                txt = fh.read()
                for m in re.finditer(r"\$([A-Za-z_][A-Za-z0-9_]*)", txt):
                    vars_in_file.add(m.group(1))
        except Exception:
            vars_in_file = set()

        # set entries from var_map first
        for k, v in (var_map or {}).items():
            if not k:
                continue
            env_k = k[1:] if k.startswith("$") else k
            try:
                abs_v = os.path.abspath(v) if v else v
                if abs_v and os.path.abspath(file_path) == abs_v:
                    logger.warning(
                        "Skipping var_map %s -> %s because it would cause recursive include",
                        env_k,
                        v,
                    )
                    continue
            except Exception:
                pass
            old_env[env_k] = os.environ.get(env_k)
            try:
                if v is None:
                    pass
                else:
                    os.environ[env_k] = v
                    logger.info("Set environment %s -> %s (from var_map)", env_k, v)
            except Exception:
                pass

        # heuristics for variables that appear in the file but are not in env
        for var in vars_in_file:
            if var in os.environ:
                continue
            if var.startswith('COMPONENT_KCONFIGS') and 'COMPONENT_KCONFIGS_SOURCE_FILE' in (var_map or {}):
                val = (var_map or {}).get('COMPONENT_KCONFIGS_SOURCE_FILE')
                if val:
                    old_env[var] = os.environ.get(var)
                    try:
                        os.environ[var] = val
                        logger.info(
                            "Set environment %s -> %s (heuristic from COMPONENT_KCONFIGS_SOURCE_FILE)", var, val
                        )
                    except Exception:
                        pass
                    continue
            if var.lower() in ('srctree', 'srctree_dir'):
                val = os.path.dirname(os.path.abspath(file_path))
                old_env[var] = os.environ.get(var)
                try:
                    os.environ[var] = val
                    logger.info("Set environment %s -> %s (srctree fallback)", var, val)
                except Exception:
                    pass

        # ensure srctree exists: prefer IDF_PATH if provided
        srctree_key = 'srctree'
        if srctree_key not in old_env:
            old_env[srctree_key] = os.environ.get(srctree_key)
        try:
            if (var_map or {}).get('IDF_PATH'):
                os.environ[srctree_key] = os.path.abspath((var_map or {}).get('IDF_PATH'))
            else:
                os.environ[srctree_key] = os.path.dirname(os.path.abspath(file_path))
        except Exception:
            pass

        return old_env, vars_in_file

    def _preprocess_file(self, file_path: str, search_paths: List[str]) -> str:
        """Preprocess includes and component expansion. Returns path to file to pass to kconfiglib.

        May write a temp merged file and return its name. If no preprocessing
        is needed, returns the original file_path.
        """
        file_to_parse = file_path
        try:
            with open(file_path, 'r', encoding='utf-8') as fh:
                txt = fh.read()
            import re

            # Helper: resolve include path to absolute to avoid temp-dir relative resolution
            def _abs_include(p_in: str, kw: str) -> str:
                try:
                    p_exp = os.path.expandvars(p_in)
                except Exception:
                    p_exp = p_in
                if os.path.isabs(p_exp):
                    return p_exp.replace('\\', '/')
                base = os.path.dirname(file_path) if kw in ('rsource', 'orsource') else os.environ.get('srctree') or os.path.dirname(file_path)
                try:
                    abs_p = os.path.abspath(os.path.join(base, p_exp))
                except Exception:
                    abs_p = p_exp
                return abs_p.replace('\\', '/')

            # Helper: in-place normalize a Kconfig file to be parser-friendly
            def _normalize_kconfig_file(src_path: str, cache: Dict[str, str]) -> str:
                import tempfile
                if src_path in cache:
                    return cache[src_path]
                try:
                    with open(src_path, 'r', encoding='utf-8', errors='ignore') as sf:
                        lines = sf.readlines()
                except Exception:
                    return src_path

                def _abs_include_for_src(p_in: str, kw: str, base_src: str) -> str:
                    try:
                        p_exp = os.path.expandvars(p_in)
                    except Exception:
                        p_exp = p_in
                    if os.path.isabs(p_exp):
                        return p_exp.replace('\\', '/')
                    base = os.path.dirname(base_src) if kw in ('rsource', 'orsource') else os.environ.get('srctree') or os.path.dirname(base_src)
                    try:
                        abs_p = os.path.abspath(os.path.join(base, p_exp))
                    except Exception:
                        abs_p = p_exp
                    return abs_p.replace('\\', '/')

                new_lines = []
                menu_depth = 0
                expecting_symbol_help = False
                in_menu_help_comment = False
                for L in lines:
                    # track structure
                    if re.match(r'^\s*menu\b', L):
                        menu_depth += 1
                        expecting_symbol_help = False
                    elif re.match(r'^\s*endmenu\b', L):
                        menu_depth = max(menu_depth - 1, 0)
                        expecting_symbol_help = False
                    elif re.match(r'^\s*(config|menuconfig|choice)\b', L):
                        expecting_symbol_help = True
                    elif re.match(r'^\s*(endchoice|endif)\b', L):
                        expecting_symbol_help = False

                    if in_menu_help_comment:
                        if L.strip() == '':
                            new_lines.append(L)
                            in_menu_help_comment = False
                            continue
                        new_lines.append('# ' + L.lstrip())
                        continue

                    if re.match(r'^\s*help\b', L):
                        if menu_depth > 0 and not expecting_symbol_help:
                            # help under a menu is not supported by kconfiglib; turn it into comments until blank line
                            new_lines.append('# help\n')
                            in_menu_help_comment = True
                            continue
                        else:
                            # symbol/choice help: pass through untouched
                            new_lines.append(L)
                            expecting_symbol_help = False
                            continue

                    mm2 = re.match(r'^\s*(?P<kw>source|osource|rsource|orsource)\s+"([^"]+)"', L)
                    if mm2:
                        kw2 = mm2.group('kw')
                        pin2 = mm2.group(2)
                        abs2 = _abs_include_for_src(pin2, kw2, src_path)
                        if kw2 == 'rsource':
                            new_lines.append(f'source "{abs2}"\n')
                        elif kw2 in ('osource', 'orsource'):
                            new_lines.append(f'osource "{abs2}"\n')
                        else:
                            new_lines.append(f'source "{abs2}"\n')
                        continue
                    new_lines.append(L)
                try:
                    tmpfd2, tmpname2 = tempfile.mkstemp(prefix='kconf_norm_', suffix='.Kconfig')
                    with open(tmpfd2, 'w', encoding='utf-8', errors='ignore') as nf:
                        nf.writelines(new_lines)
                    norm_path = tmpname2.replace('\\', '/')
                    cache[src_path] = norm_path
                    # copy to workspace for debugging
                    try:
                        dbg = os.path.join(os.path.dirname(__file__), '..', 'tmp_debug_norm.Kconfig')
                        dbg = os.path.abspath(dbg)
                        with open(norm_path, 'r', encoding='utf-8', errors='ignore') as rf, open(dbg, 'w', encoding='utf-8', errors='ignore') as wf:
                            wf.write(rf.read())
                        logger.info('Saved normalized Kconfig for debugging: %s (source: %s)', dbg, src_path)
                    except Exception:
                        pass
                    return cache[src_path]
                except Exception:
                    return src_path

            # Detect whether $COMPONENT_KCONFIGS needs expansion
            needs_expand = False
            for m in re.finditer(r'(?:^|\n)\s*source\s+"\$COMPONENT_KCONFIGS[^"]*"', txt):
                needs_expand = True
                break
            comp_kconfigs: List[str] = []
            if needs_expand:
                for sp in search_paths:
                    try:
                        pattern = os.path.join(sp, 'components', '**', 'Kconfig')
                        for p in glob.glob(pattern, recursive=True):
                            if os.path.isfile(p) and p not in comp_kconfigs:
                                comp_kconfigs.append(p)
                    except Exception:
                        pass

            if needs_expand or re.search(r'(?:^|\n)\s*(?:source|osource|rsource|orsource)\s+"', txt):
                import tempfile

                tmpfd, tmpname = tempfile.mkstemp(prefix='kconfig_merged_', suffix='.Kconfig')
                try:
                    with open(tmpfd, 'w', encoding='utf-8') as out_f:
                        written_sources = set()
                        norm_cache: Dict[str, str] = {}
                        for line in txt.splitlines(True):
                            mm = re.match(r'\s*(?P<kw>source|osource|rsource|orsource)\s+"([^"]+)"', line)
                            if mm:
                                kw = mm.group('kw')
                                path_in = mm.group(2)
                                # Expand $COMPONENT_KCONFIGS into normalized absolute source statements (no inlining)
                                if re.match(r'^\$COMPONENT_KCONFIGS', path_in) and comp_kconfigs:
                                    for ck in comp_kconfigs:
                                        ck_norm = ck.replace('\\', '/')
                                        if ck_norm in written_sources:
                                            continue
                                        written_sources.add(ck_norm)
                                        norm_path = _normalize_kconfig_file(ck_norm, norm_cache)
                                        out_f.write(f'source "{norm_path}"\n')
                                    continue
                                # Rewrite include path to absolute to avoid relative-to-temp
                                abs_p = _abs_include(path_in, kw)
                                # For regular includes, still output as absolute source/osource
                                if kw == 'rsource':
                                    out_f.write(f'source "{abs_p}"\n')
                                elif kw in ('osource', 'orsource'):
                                    out_f.write(f'osource "{abs_p}"\n')
                                else:
                                    out_f.write(f'source "{abs_p}"\n')
                                continue
                            out_f.write(line)
                    # Remove endmenu balancing to avoid mismatches across includes
                    file_to_parse = tmpname
                    logger.info(
                        'Wrote preprocessed Kconfig to %s (component includes: %d)',
                        tmpname,
                        len(comp_kconfigs),
                    )
                except Exception:
                    try:
                        os.remove(tmpname)
                    except Exception:
                        pass
        except Exception:
            file_to_parse = file_path
        return file_to_parse

    def _safe_parse_kconf(self, file_to_parse: str):
        """Attempt to parse with kconfiglib and return (kconf, orig_parse_help, patched)."""
        orig_parse_help = None
        patched = False
        try:
            try:
                orig_parse_help = getattr(kconfiglib.Kconfig, '_parse_help', None)
            except Exception:
                orig_parse_help = getattr(kconfiglib, '_parse_help', None)
            if callable(orig_parse_help):
                def _safe_parse_help(self, node):
                    try:
                        return orig_parse_help(self, node)
                    except AttributeError:
                        try:
                            if not hasattr(node, 'help'):
                                try:
                                    setattr(node, 'help', None)
                                except Exception:
                                    pass
                        except Exception:
                            pass
                        return None
                try:
                    try:
                        kconfiglib.Kconfig._parse_help = _safe_parse_help
                    except Exception:
                        kconfiglib._parse_help = _safe_parse_help
                    patched = True
                except Exception:
                    patched = False
            kconf = kconfiglib.Kconfig(file_to_parse)
            return kconf, orig_parse_help, patched
        finally:
            try:
                if patched and callable(orig_parse_help):
                    try:
                        kconfiglib.Kconfig._parse_help = orig_parse_help
                    except Exception:
                        kconfiglib._parse_help = orig_parse_help
            except Exception:
                pass

    def _traverse_top_nodes(self, kconf, items: List[Any], file_to_parse: str) -> List[Any]:
        """Traverse top nodes and populate items via handle_node mapping."""
        def gen_id(prefix, idx=[0]):
            idx[0] += 1
            return f"item-{idx[0]:06d}"

        from kconfiglib import MenuNode, Choice

        def map_node(node, parent_item=None):
            if isinstance(node, MenuNode):
                mid = gen_id('menu')
                m = MenuItem(id=mid, name=f"menu_{len(self.menu_items)+1}", prompt=getattr(node, 'prompt', '') or '')
                self.menu_items[mid] = m
                if parent_item is not None and hasattr(parent_item, 'children'):
                    parent_item.children.append(m)
                else:
                    items.append(m)
                child_ptr = getattr(node, 'list', None)
                while child_ptr is not None:
                    try:
                        kid = getattr(child_ptr, 'item', None) or child_ptr
                    except Exception:
                        kid = child_ptr
                    if kid is not None:
                        map_node(kid, m)
                    child_ptr = getattr(child_ptr, 'next', None)

            elif isinstance(node, Choice):
                cid = gen_id('choice')
                c = ChoiceItem(id=cid, prompt=getattr(node, 'prompt', '') or '')
                self.choice_items[cid] = c
                if parent_item is not None and hasattr(parent_item, 'children'):
                    parent_item.children.append(c)
                else:
                    items.append(c)
                if hasattr(node, 'syms') and node.syms:
                    for sym in node.syms:
                        map_node(sym, c)
                else:
                    child_ptr = getattr(node, 'list', None)
                    while child_ptr is not None:
                        try:
                            kid = getattr(child_ptr, 'item', None) or child_ptr
                        except Exception:
                            kid = child_ptr
                        if kid is not None:
                            map_node(kid, c)
                        child_ptr = getattr(child_ptr, 'next', None)

            elif isinstance(node, kconfiglib.Symbol):
                cfg_id = gen_id('cfg')
                # Normalize type to lowercase string expected by GUI TypeModule
                type_str = None
                try:
                    type_str = getattr(node, 'type_str', None)
                except Exception:
                    type_str = None
                if not type_str:
                    try:
                        type_str = kconfiglib.TYPE_TO_STR.get(getattr(node, 'type', None))
                    except Exception:
                        type_str = None
                # symbol properties handled in unified block below
                cfg_id = gen_id('cfg')
                # Normalize type to lowercase string expected by GUI TypeModule
                type_str = None
                try:
                    type_str = getattr(node, 'type_str', None)
                except Exception:
                    type_str = None
                if not type_str:
                    try:
                        type_str = kconfiglib.TYPE_TO_STR.get(getattr(node, 'type', None))
                    except Exception:
                        type_str = None
                if isinstance(type_str, str):
                    type_str = type_str.lower()
                prop = ConfigProperty(type=type_str)
                cfg = ConfigItem(id=cfg_id, name=getattr(node, 'name', None), properties=prop)
                self.config_items[cfg_id] = cfg
                if parent_item is not None and hasattr(parent_item, 'children'):
                    parent_item.children.append(cfg)
                else:
                    items.append(cfg)

                # Prompt handling (tuple or string)
                try:
                    p = getattr(node, 'prompt', None)
                    if p is None and hasattr(node, 'get_prompt'):
                        try:
                            p = node.get_prompt()
                        except Exception:
                            p = None
                    if isinstance(p, tuple) and p:
                        try:
                            prop.prompt = str(p[0])
                        except Exception:
                            prop.prompt = None
                    else:
                        prop.prompt = str(p) if p is not None else None
                except Exception:
                    pass

                # Defaults mapping
                try:
                    if getattr(node, 'defaults', None):
                        d = node.defaults[0]
                        val = None
                        try:
                            kconf_obj = getattr(node, 'kconfig', None)
                            expr = getattr(d, 'sym', None) or getattr(d, 'expr', None)
                            if kconf_obj is not None and expr is not None:
                                val = kconf_obj.expr_str(expr)
                            elif getattr(d, 'value', None) is not None:
                                val = str(d.value)
                        except Exception:
                            val = None
                        prop.default = DefaultValue(value=val)
                except Exception:
                    pass

                # depends on
                try:
                    dep = getattr(node, 'direct_dep', None) or getattr(node, 'dep', None)
                    if dep is not None:
                        try:
                            kconf_obj = getattr(node, 'kconfig', None)
                            expr_s = kconf_obj.expr_str(dep) if kconf_obj is not None else str(dep)
                        except Exception:
                            expr_s = str(dep)
                        prop.depends_on = Condition(expression=expr_s)
                except Exception:
                    pass

                # visible if
                try:
                    vis = getattr(node, 'visible_if', None)
                    if vis is not None:
                        try:
                            kconf_obj = getattr(node, 'kconfig', None)
                            expr_s = kconf_obj.expr_str(vis) if kconf_obj is not None else str(vis)
                        except Exception:
                            expr_s = str(vis)
                        prop.visible_if = Condition(expression=expr_s)
                except Exception:
                    pass

                # help text
                try:
                    help_txt = None
                    if hasattr(node, 'help'):
                        help_txt = getattr(node, 'help')
                    elif hasattr(node, 'get_help'):
                        try:
                            help_txt = node.get_help()
                        except Exception:
                            help_txt = None
                    if help_txt is not None:
                        try:
                            prop.help = str(help_txt)
                        except Exception:
                            prop.help = None
                except Exception:
                    pass

                # ranges (map the first range; parse numeric literals)
                try:
                    rngs = getattr(node, 'ranges', None)
                    if rngs:
                        r0 = rngs[0]
                        kconf_obj = getattr(node, 'kconfig', None)

                        def _to_int(expr) -> int | None:
                            if expr is None:
                                return None
                            try:
                                s = kconf_obj.expr_str(expr) if kconf_obj is not None else str(expr)
                            except Exception:
                                s = str(expr)
                            s = (s or '').strip()
                            try:
                                if s.lower().startswith('0x'):
                                    return int(s, 16)
                                return int(s)
                            except Exception:
                                return None

                        min_v = _to_int(getattr(r0, 'lower', None) or getattr(r0, 'min', None) or getattr(r0, 'start', None))
                        max_v = _to_int(getattr(r0, 'upper', None) or getattr(r0, 'max', None) or getattr(r0, 'end', None))

                        cond_expr = None
                        cond = getattr(r0, 'cond', None) or getattr(r0, 'condition', None)
                        if cond is not None:
                            try:
                                cond_expr = kconf_obj.expr_str(cond) if kconf_obj is not None else str(cond)
                            except Exception:
                                cond_expr = str(cond)
                        if min_v is not None and max_v is not None:
                            prop.range = RangeRestriction(min=min_v, max=max_v, condition=Condition(expression=cond_expr) if cond_expr else None)
                except Exception:
                    pass
            else:
                try:
                    if isinstance(node, (int, float)):
                        return
                except Exception:
                    pass
                try:
                    txt = str(node)
                except Exception:
                    txt = ''
                cid = gen_id('cmt')
                cmt = CommentItem(id=cid, text=txt)
                if parent_item is not None and hasattr(parent_item, 'children'):
                    parent_item.children.append(cmt)
                else:
                    items.append(cmt)

        # traverse top nodes
        try:
            top_list = getattr(kconf.top_node, 'list', None)
            if top_list is not None:
                ptr = top_list
                while ptr is not None:
                    try:
                        node = getattr(ptr, 'item', None) or ptr
                    except Exception:
                        node = ptr
                    if node is not None:
                        map_node(node, None)
                    ptr = getattr(ptr, 'next', None)
            else:
                try:
                    for node in getattr(kconf.top_node, 'list', []):
                        map_node(node, None)
                except Exception:
                    logger.exception('kconfiglib parse traversal failed: no iterable children found')
        except Exception as e:
            logger.exception('kconfiglib parse traversal failed: %s', e)

        return items

    def parse_file(self, file_path: str, search_paths: Optional[List[str]] = None, var_map: Optional[Dict[str, str]] = None):
        """Main entry: parse file and return (wrapper, items) or (None, [], meta) on failure.

        This mirrors the previous behavior but uses helper methods above.
        """
        if search_paths is None:
            search_paths = [os.path.dirname(os.path.abspath(file_path))]

        old_env = {}
        try:
            # Prepare env and preprocess file
            try:
                old_env, _ = self._prepare_env(file_path, var_map)
            except Exception:
                old_env = {}

            file_to_parse = self._preprocess_file(file_path, search_paths)

            # parse safely
            try:
                kconf, orig_parse_help, patched = self._safe_parse_kconf(file_to_parse)
            except Exception as e:
                logger.exception("kconfiglib failed to parse %s: %s", file_to_parse, e)
                try:
                    import re

                    msg = str(e)
                    vars_found = set(re.findall(r"\$([A-Za-z_][A-Za-z0-9_]*)", msg))
                except Exception:
                    vars_found = set()

                try:
                    if file_to_parse != file_path:
                        try:
                            import shutil

                            dbg_target = os.path.join(os.path.dirname(__file__), '..', 'tmp_debug_merged.Kconfig')
                            dbg_target = os.path.abspath(dbg_target)
                            shutil.copyfile(file_to_parse, dbg_target)
                            logger.info('Copied merged Kconfig to workspace for debugging: %s', dbg_target)
                        except Exception:
                            pass
                        try:
                            os.remove(file_to_parse)
                        except Exception:
                            pass
                except Exception:
                    pass

                try:
                    import re

                    m = re.search(r"(.*):(\d+):", str(e))
                    if m:
                        err_file = m.group(1)
                        err_line = int(m.group(2))
                        try:
                            with open(err_file, 'r', encoding='utf-8', errors='ignore') as ef:
                                lines = ef.readlines()
                            start = max(0, err_line - 6)
                            end = min(len(lines), err_line + 5)
                            ctx = ''.join(f"{i+1}: {lines[i]}" for i in range(start, end))
                            logger.error('Error context for %s (around line %d):\n%s', err_file, err_line, ctx)
                        except Exception:
                            pass
                except Exception:
                    pass

                meta = {"error": str(e), "missing_vars": list(vars_found), "merged_file": file_to_parse}
                return None, [], meta

            # traverse into items
            items: List[Any] = []
            try:
                items = self._traverse_top_nodes(kconf, [], file_to_parse)
            except Exception:
                items = []

            # wrapper similar to prior implementation
            class AdapterWrapper:
                def __init__(self, adapter: 'KconfiglibAdapter'):
                    self.config_items = adapter.config_items
                    self.menu_items = adapter.menu_items
                    self.comment_items = adapter.comment_items
                    self.choice_items = adapter.choice_items
                    self.expression_evaluator = ExpressionEvaluator()

                def evaluate_conditions(self, config_values: Dict[str, Any]) -> None:
                    for config_item in self.config_items.values():
                        if config_item.properties.depends_on:
                            try:
                                config_item.properties.depends_on.evaluated = (
                                    self.expression_evaluator.evaluate(
                                        config_item.properties.depends_on.expression, config_values
                                    )
                                )
                                config_item.is_enabled = config_item.properties.depends_on.evaluated
                            except Exception:
                                pass

                        if config_item.properties.visible_if:
                            try:
                                config_item.properties.visible_if.evaluated = (
                                    self.expression_evaluator.evaluate(
                                        config_item.properties.visible_if.expression, config_values
                                    )
                                )
                                config_item.is_visible = config_item.properties.visible_if.evaluated
                            except Exception:
                                pass

            wrapper = AdapterWrapper(self)

            # ensure each symbol known to kconfiglib is represented
            try:
                def gen_id(prefix, idx=[0]):
                    idx[0] += 1
                    return f"item-{idx[0]:06d}"

                sym_names_existing = set()
                for ci in list(self.config_items.values()):
                    try:
                        sym_names_existing.add(ci.name)
                    except Exception:
                        pass

                for sym in getattr(kconf, 'symbols', []):
                    try:
                        sname = getattr(sym, 'name', None)
                        if not sname or sname in sym_names_existing:
                            continue

                        cfg_id = gen_id('cfg')
                        prop = ConfigProperty(type=getattr(sym.type, 'name', None) if getattr(sym, 'type', None) else None)
                        cfg = ConfigItem(id=cfg_id, name=sname, properties=prop)
                        try:
                            p = getattr(sym, 'prompt', None)
                            if p is None and hasattr(sym, 'get_prompt'):
                                try:
                                    p = sym.get_prompt()
                                except Exception:
                                    p = None
                            prop.prompt = str(p) if p is not None else None
                        except Exception:
                            pass
                        try:
                            h = getattr(sym, 'help', None)
                            if h is None and hasattr(sym, 'get_help'):
                                try:
                                    h = sym.get_help()
                                except Exception:
                                    h = None
                            if h is not None:
                                prop.help = str(h)
                        except Exception:
                            pass

                        try:
                            if getattr(sym, 'defaults', None):
                                d = sym.defaults[0]
                                val = None
                                if getattr(d, 'expr', None) is not None:
                                    val = str(d.expr)
                                prop.default = DefaultValue(value=val)
                        except Exception:
                            pass

                        try:
                            if getattr(sym, 'dep', None):
                                prop.depends_on = Condition(expression=str(sym.dep))
                        except Exception:
                            pass
                        try:
                            if getattr(sym, 'visible_if', None):
                                prop.visible_if = Condition(expression=str(sym.visible_if))
                        except Exception:
                            pass

                        self.config_items[cfg_id] = cfg
                        items.append(cfg)
                        sym_names_existing.add(sname)
                    except Exception:
                        pass
            except Exception:
                pass

            # fallback scanner
            try:
                if not items:
                    import re

                    scanned_items = []
                    last_menu = None
                    with open(file_to_parse, 'r', encoding='utf-8') as fh:
                        for ln in fh:
                            ln_strip = ln.strip()
                            m = re.match(r'^menu\s+"([^"]+)"', ln_strip)
                            if m:
                                mid = gen_id('menu')
                                menu = MenuItem(id=mid, name=f"menu_{len(self.menu_items)+1}", prompt=m.group(1))
                                self.menu_items[mid] = menu
                                scanned_items.append(menu)
                                last_menu = menu
                                continue

                            m = re.match(r'^config\s+([A-Za-z0-9_]+)', ln_strip)
                            if m:
                                cfg_name = m.group(1)
                                cfg_id = gen_id('cfg')
                                prop = ConfigProperty(type=None)
                                cfg = ConfigItem(id=cfg_id, name=cfg_name, properties=prop)
                                self.config_items[cfg_id] = cfg
                                if last_menu is not None:
                                    last_menu.children.append(cfg)
                                else:
                                    scanned_items.append(cfg)
                                continue

                            if re.match(r'^choice\b', ln_strip):
                                cid = gen_id('choice')
                                c = ChoiceItem(id=cid, prompt='Choice')
                                self.choice_items[cid] = c
                                if last_menu is not None:
                                    last_menu.children.append(c)
                                else:
                                    scanned_items.append(c)
                                continue

                    if scanned_items:
                        items = scanned_items
                        logger.info('Fallback scanner produced %d items from %s', len(items), file_to_parse)
            except Exception:
                pass

            # Post-process: merge top-level menus with identical prompts
            try:
                top_menus = [it for it in items if isinstance(it, MenuItem)]
                seen = {}
                for m in top_menus:
                    key = (m.prompt or '').strip()
                    if not key:
                        continue
                    if key in seen:
                        target = seen[key]
                        existing_names = set()
                        for ch in target.children:
                            try:
                                if hasattr(ch, 'name') and ch.name:
                                    existing_names.add(ch.name)
                            except Exception:
                                pass
                        for ch in m.children:
                            try:
                                if hasattr(ch, 'name') and ch.name and ch.name in existing_names:
                                    continue
                            except Exception:
                                pass
                            target.children.append(ch)
                        try:
                            items.remove(m)
                        except Exception:
                            pass
                    else:
                        seen[key] = m
            except Exception:
                pass

            return wrapper, items
        finally:
            try:
                for ek, ov in old_env.items():
                    if ov is None:
                        try:
                            del os.environ[ek]
                        except Exception:
                            pass
                    else:
                        try:
                            os.environ[ek] = ov
                        except Exception:
                            pass
            except Exception:
                pass

        # Fallback: if we didn't build any config/menu items (kconfiglib may have
        # emitted only comments/warnings for malformed example files), do a
        # lightweight regex scan of the preprocessed file to create minimal
        # placeholders so the GUI has something to show.
        try:
            if not items:
                try:
                    import re
                    scanned_items = []
                    last_menu = None
                    with open(file_to_parse, 'r', encoding='utf-8') as fh:
                        for ln in fh:
                            ln_strip = ln.strip()
                            m = re.match(r'^menu\s+"([^"]+)"', ln_strip)
                            if m:
                                mid = gen_id('menu')
                                menu = MenuItem(id=mid, name=f"menu_{len(self.menu_items)+1}", prompt=m.group(1))
                                self.menu_items[mid] = menu
                                scanned_items.append(menu)
                                last_menu = menu
                                continue

                            m = re.match(r'^config\s+([A-Za-z0-9_]+)', ln_strip)
                            if m:
                                cfg_name = m.group(1)
                                cfg_id = gen_id('cfg')
                                prop = ConfigProperty(type=None)
                                cfg = ConfigItem(id=cfg_id, name=cfg_name, properties=prop)
                                self.config_items[cfg_id] = cfg
                                if last_menu is not None:
                                    last_menu.children.append(cfg)
                                else:
                                    scanned_items.append(cfg)
                                continue

                            # simple choice detection
                            if re.match(r'^choice\b', ln_strip):
                                cid = gen_id('choice')
                                c = ChoiceItem(id=cid, prompt='Choice')
                                self.choice_items[cid] = c
                                if last_menu is not None:
                                    last_menu.children.append(c)
                                else:
                                    scanned_items.append(c)
                                continue

                    if scanned_items:
                        items = scanned_items
                        logger.info('Fallback scanner produced %d items from %s', len(items), file_to_parse)
                except Exception:
                    pass
        except Exception:
            pass

            # Post-process: merge top-level menus that have identical prompt text
            try:
                # collect menus at top-level in items
                top_menus = [it for it in items if isinstance(it, MenuItem)]
                seen = {}
                for m in top_menus:
                    key = (m.prompt or '').strip()
                    if not key:
                        continue
                    if key in seen:
                        # merge children from m into seen[key]
                        target = seen[key]
                        existing_names = set()
                        for ch in target.children:
                            try:
                                if hasattr(ch, 'name') and ch.name:
                                    existing_names.add(ch.name)
                            except Exception:
                                pass
                        for ch in m.children:
                            try:
                                # avoid adding duplicate configs by symbol name
                                if hasattr(ch, 'name') and ch.name and ch.name in existing_names:
                                    continue
                            except Exception:
                                pass
                            target.children.append(ch)
                        # remove m from items list
                        try:
                            items.remove(m)
                        except Exception:
                            pass
                    else:
                        seen[key] = m
            except Exception:
                pass

            return wrapper, items
